package myicesscheduler.web;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.DecimalFormat;
import java.util.LinkedList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class MyFileCompressor
{
   private static final Log logger = LogFactory.getLog( MyFileCompressor.class );

   public enum CompressedFilenamePostfixType
   {
      THREE_DIGIT_START_FROM_ZERO
   }

   // Ignore "." and ".." folders.
   public static String[] compressFiles( String compressedFilename, String rootOfFilesToCompress,
         String[] filesToCompress, int minSizeInByte,
         final CompressedFilenamePostfixType postfixType )
   {
      final int totalRequstToCompress = filesToCompress.length;
      logger.info( "Total request to compress: " + totalRequstToCompress );
      if( totalRequstToCompress == 0 )
      {
         logger.warn( "No files to compress." );
         return new String[ 0 ];
      }

      int compressedFiles = 0;
      final MultiZip zips = new MultiZip( compressedFilename, postfixType, minSizeInByte );
      try
      {
         for( final String fullPathToCompress : filesToCompress )
         {
            final String relativePathToCompress = MyFileUtil.getSubPath( rootOfFilesToCompress,
                  fullPathToCompress );
            logger.info( "Compressing a file: " + fullPathToCompress );

            final FileInputStream srcFIS = FileInputStream_open( fullPathToCompress );
            if( null == srcFIS ) continue;
            try
            {
               final byte data[] = new byte[ 512 ];
               final BufferedInputStream src = new BufferedInputStream( srcFIS, data.length );
               if( null == src ) continue;
               try
               {
                  final boolean bPutNextEntry = zips.putNextEntry( new ZipEntry(
                        relativePathToCompress ) );
                  if( false == bPutNextEntry ) continue;

                  for( int count; ( count = BufferedInputStream_read( src, data ) ) != -1; )
                  {
                     final boolean bWrite = zips.write( data, 0, count );
                     if( false == bWrite ) break;
                  }
               } finally
               {
                  BufferedInputStream_close( src );
               }
            } finally
            {
               FileInputStream_close( srcFIS );
            }
         }
      } finally
      {
         zips.flush();
         zips.close();
      }
      return zips.getZipFilenames();
   }

   // This implementation for ZIP splitting is entry based.
   // In other words, this cannot generate several ZIP files if we are trying to compress a big file.
   // This can be a problem for a big database file if the web hosting service limits the size of upload/download.
   // The reason why I am still using entry based ZIP splitting is
   // because it decouples the order dependency between split ZIP files.
   static private class MultiZip
   {
      private final int minSumofUncompressedSize;
      private final String zipFilenameBase;
      private final CompressedFilenamePostfixType postfixType;

      private ZipOutputStream destZip;
      private final List< String > zipFilenames;
      private int sumofUncompressedSizeOnEachZip;

      public MultiZip( final String zipFilenameBase,
            final CompressedFilenamePostfixType postfixType, final int minSizeInByte )
      {
         this.zipFilenameBase = zipFilenameBase;
         this.minSumofUncompressedSize = minSizeInByte;
         this.postfixType = postfixType;
         this.destZip = null;
         this.zipFilenames = new LinkedList< String >();

         openNextZip();
      }

      public ZipOutputStream openNextZip()
      {
         if( null != this.destZip )
         {
            flush();
            close();
         }

         final String eachZipFilename = getZipFilename( this.zipFilenameBase, this.zipFilenames
               .size(), this.postfixType );

         FileOutputStream destFile;
         try
         {
            destFile = new FileOutputStream( eachZipFilename );
         } catch( FileNotFoundException e1 )
         {
            e1.printStackTrace();
            logger.error( "Zip file not found: " + eachZipFilename );
            return null;
         }

         final BufferedOutputStream destBuffer = new BufferedOutputStream( destFile );
         final ZipOutputStream destZip = new ZipOutputStream( destBuffer );
         destZip.setMethod( ZipOutputStream.DEFLATED );

         this.destZip = destZip;
         this.zipFilenames.add( eachZipFilename );
         this.sumofUncompressedSizeOnEachZip = 0;
         return destZip;
      }

      public boolean putNextEntry( ZipEntry zipEntry )
      {
         if( this.sumofUncompressedSizeOnEachZip >= this.minSumofUncompressedSize ) openNextZip();

         try
         {
            destZip.putNextEntry( zipEntry );
         } catch( IOException e )
         {
            e.printStackTrace();
            logger.error( "Zip file cannot perform putNextEntry." );
            return false;
         }
         return true;
      }

      public boolean write( byte[] data, int pos, int count )
      {
         try
         {
            destZip.write( data, pos, count );
         } catch( IOException e )
         {
            e.printStackTrace();
            logger.error( "Zip file cannot be written." );
            return false;
         }

         this.sumofUncompressedSizeOnEachZip += count;
         return true;
      }

      public boolean flush()
      {
         try
         {
            destZip.flush();
         } catch( IOException e )
         {
            e.printStackTrace();
            logger.error( "Zip file cannot be flushed." );
            return false;
         }
         return true;
      }

      public boolean close()
      {
         try
         {
            destZip.close();
         } catch( IOException e )
         {
            e.printStackTrace();
            logger.error( "Zip file cannot be closed." );
            return false;
         }
         return true;
      }

      public String[] getZipFilenames()
      {
         return (String[]) zipFilenames.toArray( new String[ zipFilenames.size() ] );
      }
   }

   public static String getZipFilename( String compressedFilename, int idx,
         CompressedFilenamePostfixType postfixType )
   {
      final String ext = "zip";

      // currently only one type is implemented.
      // if( postfixType.equals( CompressedFilenamePostfixType.THREE_DIGIT_START_FROM_ZERO ) )
      {
         final DecimalFormat fmt = new DecimalFormat( "000" );
         final String id = fmt.format( idx );
         return compressedFilename + "." + id + "." + ext;
      }
   }

   private static void FileInputStream_close( final FileInputStream srcFIS )
   {
      try
      {
         srcFIS.close();
      } catch( IOException e )
      {
         e.printStackTrace();
      }
   }

   private static void BufferedInputStream_close( final BufferedInputStream src )
   {
      try
      {
         src.close();
      } catch( IOException e )
      {
         e.printStackTrace();
      }
   }

   private static int BufferedInputStream_read( final BufferedInputStream src, final byte[] data )
   {
      try
      {
         return src.read( data, 0, data.length );
      } catch( IOException e )
      {
         e.printStackTrace();
         logger.error( "File cannot be read." );
      }
      return 0;
   }

   private static FileInputStream FileInputStream_open( String fileToCompress )
   {
      FileInputStream fi;
      try
      {
         fi = new FileInputStream( fileToCompress );
      } catch( FileNotFoundException e )
      {
         e.printStackTrace();
         logger.error( "File not found exception: " + fileToCompress );
         return null;
      }
      return fi;
   }

   public boolean closeZip( ZipOutputStream destZip )
   {
      try
      {
         destZip.close();
      } catch( IOException e )
      {
         e.printStackTrace();
         logger.error( "Zip file cannot be closed." );
         return false;
      }
      return true;
   }
}
